<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<style>

#canvas { border: 1px solid black; }
.controls { margin-left: 5px; }
.split { display: flex; }
* { user-select: none; }


</style>
</head>
<body>

<div class="split">
  <canvas id="canvas"></canvas>
  <div>
    <div class="controls">
      <div>
        <div><input type="range" id="radius" min="2" max="40" value="16"><label for="radius">radius</label></div>
        <div><input type="range" id="hardness" min="0" max="1" step="0.01" value="0.5"><label for="radius">hardness</label></div>
        <div><input type="range" id="alpha" min="0" max="1" step="0.01" value="0.5"><label for="alpha">alpha</label></div>
        <button type="button" id="reset">reset</button>
      </div>
      <div style="text-align: right;">
        <canvas id="brush-display" width="80" height="80"></canvas>
      </div>
    </div>
  </div>
</div>


</body>
<script>

const ctx = document.querySelector('#canvas').getContext('2d');
const brushDisplayCtx = document.querySelector('#brush-display').getContext('2d');

function reset() {
  const {width, height} = ctx.canvas;
  const wd2 = width / 2
  ctx.globalAlpha = 1;
  ctx.fillStyle = 'white';
  ctx.fillRect(wd2, 0, wd2, height);

  const gradient = ctx.createLinearGradient(0, 0, 0, height);
  gradient.addColorStop(0, 'red');
  gradient.addColorStop(0.5, 'yellow');
  gradient.addColorStop(1, 'blue');
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, wd2, height);
}
reset();

function getCanvasRelativePosition(e, canvas) {
  const rect = canvas.getBoundingClientRect();
  return {
    x: (e.clientX - rect.left) / rect.width  * canvas.width,
    y: (e.clientY - rect.top ) / rect.height * canvas.height,
  };
}

function lerp(a, b, t) {
  return a + (b - a) * t;
}

function setupLine(x, y, targetX, targetY) {
  const deltaX = targetX - x;
  const deltaY = targetY - y;
  const deltaRow = Math.abs(deltaX);
  const deltaCol = Math.abs(deltaY);
  const counter = Math.max(deltaCol, deltaRow);
  const axis = counter == deltaCol ? 1 : 0;

  // setup a line draw.
  return {
    position: [x, y],
    delta: [deltaX, deltaY],
    deltaPerp: [deltaRow, deltaCol],
    inc: [Math.sign(deltaX), Math.sign(deltaY)],
    accum: Math.floor(counter / 2),
    counter: counter,
    endPnt: counter,
    axis: axis,
    u: 0,
  };
};

function advanceLine(line) {
  --line.counter;
  line.u = 1 - line.counter / line.endPnt;
  if (line.counter <= 0) {
    return false;
  }
  const axis = line.axis;
  const perp = 1 - axis;
  line.accum += line.deltaPerp[perp];
  if (line.accum >= line.endPnt) {
    line.accum -= line.endPnt;
    line.position[perp] += line.inc[perp];
  }
  line.position[axis] += line.inc[axis];
  return true;
}

let lastX;
let lastY;
let lastForce;
let drawing = false;
let alpha = 0.5;

const brushCtx = document.createElement('canvas').getContext('2d');
let featherGradient;

function createFeatherGradient(radius, hardness) {
  const innerRadius = Math.min(radius * hardness, radius - 1);
  const gradient = brushCtx.createRadialGradient(
    0, 0, innerRadius,
    0, 0, radius);
  gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
  gradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
  return gradient;
}

const radiusElem = document.querySelector('#radius');
const hardnessElem = document.querySelector('#hardness');
const alphaElem = document.querySelector('#alpha');
radiusElem.addEventListener('input', updateBrushSettings);
hardnessElem.addEventListener('input', updateBrushSettings);
alphaElem.addEventListener('input', updateBrushSettings);
document.querySelector('#reset').addEventListener('click', reset);

function updateBrushSettings() {
  const radius = radiusElem.value;
  const hardness = hardnessElem.value;
  alpha = alphaElem.value;
  featherGradient = createFeatherGradient(radius, hardness);
  brushCtx.canvas.width = radius * 2;
  brushCtx.canvas.height = radius * 2;
  
  {
    const ctx = brushDisplayCtx;
    const {width, height} = ctx.canvas;
    ctx.clearRect(0, 0, width, height);
    ctx.fillStyle = `rgba(0, 0, 0, ${alpha})`;
    ctx.fillRect(width / 2 - radius, height / 2 - radius, radius * 2, radius * 2);
    feather(ctx);
  }
}
updateBrushSettings();
 
function feather(ctx) {
  // feather the brush
  ctx.save();
  ctx.fillStyle = featherGradient;
  ctx.globalCompositeOperation = 'destination-out';
  const {width, height} = ctx.canvas;
  ctx.translate(width / 2, height / 2);
  ctx.fillRect(-width / 2, -height / 2, width, height);  
  ctx.restore();
}
 
function updateBrush(x, y) {
  let width = brushCtx.canvas.width;
  let height = brushCtx.canvas.height;
  let srcX = x - width / 2;
  let srcY = y - height / 2;
  // draw it in the middle of the brush
  let dstX = (brushCtx.canvas.width - width) / 2;
  let dstY = (brushCtx.canvas.height - height) / 2;

  // clear the brush canvas
  brushCtx.clearRect(0, 0, brushCtx.canvas.width, brushCtx.canvas.height);

  // clip the rectangle to be
  // inside
  if (srcX < 0) {
    width += srcX;
    dstX -= srcX;
    srcX = 0;
  }
  const overX = srcX + width - ctx.canvas.width;
  if (overX > 0) {
    width -= overX;
  }

  if (srcY < 0) {
    dstY -= srcY;
    height += srcY;
    srcY = 0;
  }
  const overY = srcY + height - ctx.canvas.height;
  if (overY > 0) {
    height -= overY;
  }

  if (width <= 0 || height <= 0) {
    return;
  }

  brushCtx.drawImage(
    ctx.canvas,
    srcX, srcY, width, height,
    dstX, dstY, width, height);    
  
  feather(brushCtx);
}

function start(e) {
  const pos = getCanvasRelativePosition(e, ctx.canvas);
  lastX = pos.x;
  lastY = pos.y;
  lastForce = e.force || 1;
  drawing = true;
  updateBrush(pos.x, pos.y);
}

function draw(e) {
  if (!drawing) {
    return;
  }
  const pos = getCanvasRelativePosition(e, ctx.canvas);
  const force = e.force || 1;
  
  const line = setupLine(lastX, lastY, pos.x, pos.y);  
  for (let more = true; more;) {
    more = advanceLine(line);
    ctx.globalAlpha = alpha * lerp(lastForce, force, line.u);
    ctx.drawImage(
       brushCtx.canvas,
       line.position[0] - brushCtx.canvas.width / 2,
       line.position[1] - brushCtx.canvas.height / 2);
     updateBrush(line.position[0], line.position[1]);
  } 
  lastX = pos.x;
  lastY = pos.y;
  lastForce = force;
}

function stop() {
  drawing = false;
}

window.addEventListener('mousedown', start);
window.addEventListener('mousemove', draw);
window.addEventListener('mouseup', stop);
window.addEventListener('touchstart', e => {
  e.preventDefault();
  start(e.touches[0]);
}, {passive: false});
window.addEventListener('touchmove', e => {
  e.preventDefault();
  draw(e.touches[0]);
}, {passive: false});


</script>
